"use client";
import SparkMD5 from 'spark-md5';
import { getSuffix, showTip } from "@/lib/util";
import { PromisePoolStatic } from "@/lib/util/promisePool";
import { SetFn } from "@/types";
import { sliceIsUploadReq, sliceUploadReq, sliceIntegrityCheckReq, sliceMergeReq } from "./api";
/**分片上传中使用的并发池， 使用md5进行索引，方便终止上传 */
const poolMap = new Map<string, PromisePoolStatic<string, string>>()



/**切片的类型 */
export type SliceType = {
    flag: number;
    blob: Blob;
};
/**进度条函数的类型，参数是进度，是一个小数，需要 *100 才是正常进度条 */
type OnProgress = (progress: number) => any;
/**分片上传 - 只支持单文件
 * @param file 文件
 * @param targetPath 文件目标存储地址
 * @param chunkSize 一个分片的大小
 * @param setMd5 传入回调函数，获得文件的md5
 * @param setTip 设置react state的tip，用于文字提示
 * @param onProgress 真正上传的回调，参数是进度
 * @param ownFileName 传入ownFileName时会在路径后额外拼接自定义字符串，ai换脸模块专用，特殊需求
 * @returns 上传文件的url
 */
export async function uploadBySlice(file: File, targetPath: string, chunkSize: number, setMd5: (md5: string) => void, setTip: SetFn<string>, onProgress: OnProgress, ownFileName?: string) {
    try {
        setTip("正在计算md5");
        const md5 = await getMd5(file);
        setMd5(md5)
        const isUploadedFlag = await isUploaded(file, md5, targetPath);
        if (isUploadedFlag.flag) { //已经上传过，就直接返回路径，实现秒传
            onProgress(1);
            setTip('文件上传成功');
            return isUploadedFlag.path;
        } else {
            setTip('正在进行切片');
            const sliceList = fileSlice(file, chunkSize);
            if (!isUploadedFlag.noComplete) { //没传递过的文件，才走完整的上传流程 （断点续传）
                setTip('正在进行文件上传');
                await sliceUpload(sliceList, md5, onProgress);
            } else {
                setTip('正在进行断点续传');
            }
            await integrityCheck(sliceList, md5, onProgress);
            setTip("正在合并文件");
            //传入ownFileName时会在路径后额外拼接自定义字符串，ai换脸模块专用，特殊需求
            const path = ownFileName ? await merge(file, md5, targetPath, ownFileName) : await merge(file, md5, targetPath);
            setTip("文件上传成功！");
            return path;
        }
    } catch (error: any) {
        onProgress(-1)
        return Promise.reject(error);
    }

}
/**分片上传 - 上传中断 - 传入md5 */
export const stopUpload = (md5: string) => {
    const pool = poolMap.get(md5)
    if (pool) {
        pool.stop()
    } else {
        showTip('任务已结束，中断失败', "error")
    }
}
/**获得文件的md5，用来作为唯一索引  -  文件过大时，获取md5会很长很长时间。 或者可以考虑，用 文件名+文件最后修改时间做“唯一”标识*/
const getMd5 = (file: File) => {
    return new Promise<string>(async (resolve, reject) => {
        try {
            const reader = new FileReader();
            reader.readAsArrayBuffer(file);
            // 当文件读取完成时，计算文件MD5值
            reader.onloadend = function (e) {
                if (!e.target?.result) {
                    return reject('文件读取失败');
                }
                const spark = new SparkMD5.ArrayBuffer();
                spark.append(e.target.result as ArrayBuffer);
                const md5 = spark.end();
                resolve(md5);
            };
        } catch (error) {
            reject(error);
        }
    });
};
/**秒传 - 判断文件是否上传过，如果上传过了就直接返回文件路径，不用分片即上传 */
const isUploaded = async (file: File, md5: string, targetPath: string) => {
    const suffix = getSuffix(file.name);
    const res = await sliceIsUploadReq(md5, suffix, targetPath);
    return res;
};
/**文件切片 */
const fileSlice = (file: File, chunkSize: number) => {
    const result: SliceType[] = [];
    let index = 0;
    for (let nowSize = 0; nowSize < file.size; nowSize += chunkSize) {
        result.push({
            flag: index,
            blob: file.slice(nowSize, nowSize + chunkSize),
        });
        index++;
    }
    console.log('分片数量', result.length);
    return result;
};
/**生成上传切片的promise函数 */
const getSliceUploadPromise = (slice: SliceType, md5: string) => {
    const formData = new FormData();
    formData.append(`file`, slice.blob);
    formData.append(`index`, String(slice.flag));
    formData.append(`md5`, md5);
    return () => sliceUploadReq(formData);
};
/**把所有切片上传 */
const sliceUpload = async (sliceList: SliceType[], md5: string, onProgress: OnProgress) => {
    try {
        const taskList: (() => Promise<string>)[] = [];
        const length = sliceList.length;
        for (let i = 0; i < length; i++) {
            taskList.push(getSliceUploadPromise(sliceList[i], md5));
        }
        //使用并发池优化，避免堵塞
        const promisePool = new PromisePoolStatic<string, string>()
        poolMap.set(md5, promisePool)//记录并发池
        const res = await promisePool.run(taskList, 5, (count) => onProgress(count / length));
        poolMap.delete(md5)//删除记录
        return res;
    } catch (error) {
        return Promise.reject(error)
    }

};
/**完整性校验，缺少的继续上传  （断点续传） */
const integrityCheck = async (sliceList: SliceType[], md5: string, onProgress: OnProgress) => {
    try {
        const getArr = async () => (await sliceIntegrityCheckReq(sliceList, md5)).missingArr;
        /**最大试错次数 */
        let maxCount = 5;
        /**缺少的序号数组 */
        let missingArr: number[] = await getArr();
        /**总分片数量 */
        const sliceListLength = sliceList.length;
        onProgress((sliceListLength - missingArr.length) / sliceListLength);
        while (missingArr.length) {
            const tasks: (() => Promise<string>)[] = [];
            for (let i = 0; i < missingArr.length; i++) {
                tasks.push(getSliceUploadPromise(sliceList[missingArr[i]], md5));
            }
            const promisePool = new PromisePoolStatic<string, string>()
            poolMap.set(md5, promisePool)//记录并发池
            await promisePool.run(tasks, 5, (count) => onProgress((sliceListLength - (missingArr.length - count)) / sliceListLength)); //这里的count是缺少的部分中，完成的数量。
            poolMap.delete(md5)//删除记录
            missingArr = await getArr();
            maxCount--;
            if (maxCount === 0) {
                return Promise.reject(`已尝试5次仍未成功`);
            }
        }
    } catch (error) {
        return Promise.reject(error)
    }
};
/**合并切片，拿到路径 */
const merge = async (file: File, md5: string, targetPath: string, ownFileName?: string) => {
    const suffix = getSuffix(file.name);
    const path = ownFileName ? await sliceMergeReq(md5, suffix, targetPath, ownFileName) : await sliceMergeReq(md5, suffix, targetPath);
    return path;
};

